﻿//-----------------------------------------------------------------------
// <copyright>
// Copyright (C) Sergey Solyanik for The Black Square Project.
//
// This file is subject to the terms and conditions of the Microsoft Public License (MS-PL).
// See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL for more details.
// </copyright>
//----------------------------------------------------------------------- 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using DataModel;
using DocumentManager;

/// <summary>
/// Object extensions: adds a few convenient methods to various types.
/// </summary>
public static class RuntimeExtensions
{
    /// <summary>
    /// Appends to a WebControl's list of CSS classes.
    /// </summary>
    /// <param name="ctrl"> The WebControl to append the CSS class to. </param>
    /// <param name="cssClass"> The class to append. </param>
    public static void AppendCSSClass(this WebControl ctrl, string cssClass)
    {
        if (ctrl == null)
            return;

        if (ctrl.CssClass == null || ctrl.CssClass.Equals(""))
            ctrl.CssClass = cssClass;
        else
            ctrl.CssClass = ctrl.CssClass + " " + cssClass;
    }

    /// <summary>
    /// Performs a ordinal case-insensitive equality test.
    /// </summary>
    /// <param name="lhs"> The "this" parameter </param>
    /// <param name="rhs"> The string to test for equality. </param>
    /// <returns> true if strings are equal, false otherwise. </returns>
    public static bool EqualsIgnoreCase(this String lhs, String rhs)
    {
        return lhs.Equals(rhs, StringComparison.OrdinalIgnoreCase);
    }
}

/// <summary>
/// Implementation of the Black Square document review web site.
/// </summary>
public partial class _Default : System.Web.UI.Page 
{
    // Unix epoch base for time conversions (in UTC).
    private static DateTime unixEpochOrigin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);

    // The data connection.
    private BlackSquareDataContext DataContext = new BlackSquareDataContext(
        System.Configuration.ConfigurationManager.ConnectionStrings["DataConnectionString"].ConnectionString);

    // Authenticated alias, sans domain.
    private string AuthenticatedUserAlias = null;

    // Number of file upload controls for new files.
    private int NumberOfFilesToAdd = 10;

    // Number of reviewers to add.
    private int NumberOfReviewersToAdd = 10;

    /// <summary>
    /// Overrides the dispose function to get rid of the database connection context.
    /// </summary>
    override public void Dispose()
    {
        DataContext.Connection.Close();
        DataContext.Dispose();
        base.Dispose();
    }

    /// <summary>
    /// Creates a string representation of a timestamp. This can later be processed by JavaScript to covert
    /// from UTC to local.
    /// </summary>
    /// <param name="ts"> Time stamp to wrap. </param>
    /// <returns> Html element (as string) wrapping the time stamp text. </returns>
    private static string WrapTimeStamp(DateTime ts)
    {
        TimeSpan diff = ts - unixEpochOrigin;
        return "<span name=\"timestamp\" id=\"timestamp\" ticks=\"" + Math.Floor(diff.TotalSeconds) + "\">" + ts +
            " UTC</span>";
    }

    /// <summary>
    /// Adds a text label to the end of the page.
    /// </summary>
    /// <param name="contents"></param>
    private void AddLabel(string contents, string css)
    {
        Label l = new Label();
        l.Text = contents;
        if (css != null)
            l.AppendCSSClass(css);
        ActivePage.Controls.Add(l);
    }

    /// <summary>
    /// Adds a text label to the end of the page.
    /// </summary>
    /// <param name="contents"></param>
    private void AddLabel(string contents)
    {
        AddLabel(contents, null);
    }

    /// <summary>
    /// Adds a hyperlink to the page.
    /// </summary>
    /// <param name="label"> The text label of the hyperlink. </param>
    /// <param name="query"> The query, starting with "?". </param>
    /// <param name="css"> The css class for the link. </param>
    private void AddLink(string label, string query, string css)
    {
        HyperLink link = new HyperLink();
        ActivePage.Controls.Add(link);
        link.Text = label;
        link.NavigateUrl = Request.FilePath + query;
        if (css != null)
            link.AppendCSSClass(css);
    }

    /// <summary>
    /// Adds a hyperlink to the page.
    /// </summary>
    /// <param name="label"> The text label of the hyperlink. </param>
    /// <param name="query"> The query, starting with "?". </param>
    private void AddLink(string label, string query)
    {
        AddLink(label, query, null);
    }

    /// <summary>
    /// Display the error report.
    /// </summary>
    /// <param name="errorReport"> The error to print. </param>
    private void ErrorOut(string errorReport)
    {
        AddLabel("<br><font color=red>" + errorReport + "</font><br>");
    }

    /// <summary>
    /// Displays the header for a page.
    /// </summary>
    /// <param name="title"> The title to show. </param>
    private void DisplayPageHeader(string title)
    {
        Page.Header.Title = title;

        Table t = new Table();
        ActivePage.Controls.Add(t);
        t.AppendCSSClass("CssHeader");

        TableRow r = new TableRow();
        t.Rows.Add(r);
        TableCell c = new TableCell();
        r.Cells.Add(c);
        c.Text = "<h1>" + title + "</h1>";

        c = new TableCell();
        r.Cells.Add(c);

        c.AppendCSSClass("CssHeaderMyReviews");

        HyperLink l = new HyperLink();
        l.Text = "My Dashboard";
        l.NavigateUrl = Request.FilePath;
        c.Controls.Add(l);

        c = new TableCell();
        r.Cells.Add(c);

        c = new TableCell();
        r.Cells.Add(c);

        c.AppendCSSClass("CssHeaderHelp");

        l = new HyperLink();
        l.Text = "Help";
        l.NavigateUrl = Request.FilePath + "?action=help&sourceUrl=" + Server.UrlEncode(Request.Url.ToString());
        c.Controls.Add(l);
    }

    /// <summary>
    /// Generates the page. This is the main entry point.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!HttpContext.Current.User.Identity.IsAuthenticated)
        {
            ErrorOut("I do not know you");
            return;
        }

        string userName = HttpContext.Current.User.Identity.Name;
        int bs = userName.IndexOf('\\');
        if (bs != -1)
            userName = userName.Substring(bs + 1);

        AuthenticatedUserAlias = userName;

        // Top-right corner menu actions.
        string action = Request.QueryString["action"];
        if ("help".EqualsIgnoreCase(action))
        {
            DisplayHelp(Request.QueryString["sourceUrl"]);
            return;
        }

        if ("newdocset".EqualsIgnoreCase(action))
        {
            SubmitNewDocumentSet();
            return;
        }

        // Change list display.
        string documentSetId = Request.QueryString["dsid"];
        if (documentSetId != null)
        {
            int dsid;
            if (!Int32.TryParse(documentSetId, out dsid))
            {
                ErrorOut("Document Set Id specified incorrectly. Should be a number, but is not.");
                return;
            }

            if ("makemereviewer".EqualsIgnoreCase(action))
            {
                AddToReviewers(dsid, userName);
                return;
            }
            else if ("addreviewers".EqualsIgnoreCase(action))
            {
                AddReviewers(dsid);
                return;
            }
            else if ("iterate".EqualsIgnoreCase(action))
            {
                SubmitIterationOrComment(dsid, true);
                return;
            }
            else if ("comment".EqualsIgnoreCase(action))
            {
                SubmitIterationOrComment(dsid, false);
                return;
            }
            else if ("close".EqualsIgnoreCase(action))
            {
                DataContext.SetDocumentSetStatus(dsid, 2);
                Response.Redirect(Request.FilePath + "?dsid=" + dsid);
                return;
            }
            else if ("delete".EqualsIgnoreCase(action))
            {
                DataContext.SetDocumentSetStatus(dsid, 1);
                Response.Redirect(Request.FilePath + "?dsid=" + dsid);
                return;
            }

            DisplayDocumentSet(dsid);

            return;
        }

        // Download a document.
        string versionId = Request.QueryString["vid"];
        if (versionId != null)
        {
            int vid;
            if (!int.TryParse(versionId, out vid))
            {
                ErrorOut("File version id specified incorrectly. Should be a number, but is not.");
                return;
            }

            DownloadDocument(vid, "unlock".EqualsIgnoreCase(action));
            return;
        }

        if ("allreviews".EqualsIgnoreCase(action))
        {
            DisplayReviews(true);
            return;
        }

        if ("activereviews".EqualsIgnoreCase(action))
        {
            DisplayReviews(false);
            return;
        }

        string alias = Request.QueryString["alias"];
        DisplayDashboard(alias != null ? alias : AuthenticatedUserAlias);
    }

    /// <summary>
    /// Downloads a document.
    /// </summary>
    /// <param name="vid"> Document version. </param>
    /// <param name="writeable"> True if the writeable version of the document needs to be downloaded. </param>
    private void DownloadDocument(int vid, bool writeable)
    {
        var documentVersion = (from vv in DataContext.DocumentVersions
                               where vv.Id == vid
                               join dd in DataContext.Documents on vv.DocumentId equals dd.Id
                               select new { dd.FileName, vv.Contents }).ToArray();
        if (documentVersion.Length != 1)
        {
            ErrorOut("There is nothing there.");
            return;
        }

        string tempFileName = Path.GetTempFileName();
        try
        {
            File.WriteAllBytes(tempFileName, documentVersion[0].Contents.ToArray());

            string fileName = documentVersion[0].FileName;
            if (fileName.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
            {
                if (writeable)
                    DocumentLocker.Unlock(tempFileName);
                else
                    fileName = Path.GetFileNameWithoutExtension(fileName) + "." + vid + Path.GetExtension(fileName);
            }

            Response.Clear();
            Response.AddHeader("Content-Disposition", "attachment; filename=" + fileName);
            Response.AddHeader("Content-Length", (new FileInfo(tempFileName)).Length.ToString());
            Response.ContentType = "application/octet-stream";
            Response.Cache.SetCacheability(HttpCacheability.NoCache);
            Response.WriteFile(tempFileName, true);
            Response.End();
        }
        finally
        {
            File.Delete(tempFileName);
        }
    }

    /// <summary>
    /// Uploads new document set.
    /// </summary>
    private void SubmitNewDocumentSet()
    {
        DisplayPageHeader("Upload new document set.");

        AddLabel("Document set title: ");
        TextBox title = new TextBox();
        ActivePage.Controls.Add(title);
        title.AppendCSSClass("CssTitleBox");
        title.ID = "title";
        title.TextMode = TextBoxMode.SingleLine;

        AddLabel("<br>Description:<br>");
        TextBox description = new TextBox();
        ActivePage.Controls.Add(description);
        description.AppendCSSClass("CssDescriptionBox");
        description.ID = "description";
        description.TextMode = TextBoxMode.MultiLine;

        AddLabel("<br><br>Documents:<br>");

        Table documents = new Table();
        ActivePage.Controls.Add(documents);
        documents.AppendCSSClass("CssFileLoader");

        for (int i = 0; i < NumberOfFilesToAdd; ++i)
        {
            TableRow r = new TableRow();
            documents.Rows.Add(r);
            TableCell c = new TableCell();
            r.Cells.Add(c);
            c.AppendCSSClass("CssDocumentUploaderCell");
            FileUpload u = new FileUpload();
            c.Controls.Add(u);
            u.AppendCSSClass("CssDocumentUploader");
            u.ID = "new_" + i;
        }

        Button submit = new Button();
        ActivePage.Controls.Add(submit);
        submit.AppendCSSClass("CssSubmitIteration");
        submit.ID = "submit";
        submit.Text = "Submit";
        submit.Click += new EventHandler(submitNewDocumentSet_Click);
    }

    /// <summary>
    /// Submits a new document set.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void submitNewDocumentSet_Click(object sender, EventArgs e)
    {
        TextBox titleBox = (TextBox)FindControl("title");
        string title = titleBox.Text;

        TextBox descriptionBox = (TextBox)FindControl("description");
        string description = descriptionBox.Text;

        List<HttpPostedFile> files = new List<HttpPostedFile>();
        for (int i = 0; i < NumberOfFilesToAdd; ++i)
        {
            FileUpload fu = (FileUpload)FindControl("new_" + i);
            if (fu == null)
                continue;
            if (!fu.HasFile)
                continue;

            files.Add(fu.PostedFile);
        }

        if (string.IsNullOrEmpty(title))
        {
            ErrorOut("Need a non-empty title!");
            return;
        }

        if (files.Count == 0)
        {
            ErrorOut("No files posted!");
            return;
        }

        int? dsid = null;
        DataContext.CreateNewDocumentSet(title, description, ref dsid);

        foreach (HttpPostedFile file in files)
        {
            string fileName = Path.GetFileName(file.FileName);
            int? docid = null;
            DataContext.AddDocument(fileName, dsid, ref docid);
            string tempFile = Path.GetTempFileName();
            try
            {
                file.SaveAs(tempFile);
                if (fileName.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
                    DocumentManager.DocumentLocker.Lock(tempFile);
                byte[] newFile = File.ReadAllBytes(tempFile);
                int? verid = null;
                DataContext.AddVersion(newFile, docid, ref verid);
            }
            finally
            {
                File.Delete(tempFile);
            }
        }
        
        Response.Redirect(Request.FilePath + "?dsid=" + dsid);
    }

    /// <summary>
    /// Displays user dashboard.
    /// </summary>
    private void DisplayDashboard(string userName)
    {
        DisplayPageHeader("Dashboard for " + AuthenticatedUserAlias);

        // Action bar.
        AddLink("New document set", "?action=newdocset", "CssMenuItem");
        AddLink("Active reviews", "?action=activereviews", "CssMenuItem");
        AddLink("All reviews", "?action=allreviews", "CssMenuItem");
        AddLabel("<br><br>");

        bool userIsMe = userName.EqualsIgnoreCase(AuthenticatedUserAlias);

        string usersChangesTitle = userIsMe ? "My changes" : userName + "'s changes";
        string usersReviewsTitle = userIsMe ? "My reviews" : userName + "'s reviews";
        string recentHistoryTitle = userIsMe ? "My history" : userName + "'s history";

        Table page = new Table();

        ActivePage.Controls.Add(page);
        page.EnableViewState = false;

        TableRow row = new TableRow();
        page.Rows.Add(row);
        TableCell cell = new TableCell();
        cell.Text = "<b>" + usersChangesTitle + ":</b>";
        row.Cells.Add(cell);

        DateTime historyThreshold = DateTime.Now.AddDays(-14);
        Dictionary<int, DocumentSet> allClosedChangeLists = new Dictionary<int, DocumentSet>();

        var myChangesQuery =
            from mc in DataContext.DocumentSets
            where userName.Equals(mc.UserAlias) && (mc.Status == 0 || mc.TimeStamp > historyThreshold)
            select mc;

        foreach (DocumentSet docSet in myChangesQuery)
        {
            if (docSet.Status == 0)
                BuildReviewRow(page, docSet, true, false);
            else
                allClosedChangeLists[docSet.Id] = docSet;
        }

        row = new TableRow();
        page.Rows.Add(row);
        cell = new TableCell();
        cell.Text = "<b>" + usersReviewsTitle + ":<b>";
        row.Cells.Add(cell);

        var myReviewsQuery =
            from rv in DataContext.Reviewers
            where userName.Equals(rv.UserAlias)
            join ch in DataContext.DocumentSets on rv.DocumentSetId equals ch.Id
            where (ch.Status == 0 || ch.TimeStamp > historyThreshold)
            select ch;

        foreach (DocumentSet docSet in myReviewsQuery)
        {
            if (docSet.Status == 0)
                BuildReviewRow(page, docSet, true, false);
            else
                allClosedChangeLists[docSet.Id] = docSet;
        }

        row = new TableRow();
        page.Rows.Add(row);
        cell = new TableCell();
        cell.Text = "<b>" + recentHistoryTitle + ":<b>";
        row.Cells.Add(cell);

        DocumentSet[] closedReviews = allClosedChangeLists.Values.ToArray();
        Array.Sort(closedReviews, delegate(DocumentSet c1, DocumentSet c2)
        {
            return c2.Id - c1.Id;
        });

        foreach (DocumentSet docSet in closedReviews)
            BuildReviewRow(page, docSet, true, false);
    }

    /// <summary>
    /// Displays reviews.
    /// </summary>
    private void DisplayReviews(bool displayAll)
    {
        DisplayPageHeader(displayAll ? "All reviews" : "Active reviews");

        // Action bar.
        AddLink("New document set", "?action=newdocset", "CssMenuItem");
        if (displayAll)
            AddLink("Active reviews", "?action=activereviews", "CssMenuItem");
        else
            AddLink("All reviews", "?action=allreviews", "CssMenuItem");
        AddLink("Dashboard", "", "CssMenuItem");
        AddLabel("<br><br>");

        Table page = new Table();

        ActivePage.Controls.Add(page);
        page.EnableViewState = false;

        DateTime historyThreshold = DateTime.Now.AddDays(-14);

        var changesQuery = displayAll ?
            from mc in DataContext.DocumentSets
            select mc :
            from mc in DataContext.DocumentSets
            where mc.Status == 0
            select mc;

        foreach (DocumentSet docSet in changesQuery)
            BuildReviewRow(page, docSet, false, true);
    }

    /// <summary>
    /// Populates one row of data with the details of the passed document set.
    /// Used when displaying the list of document sets in review. Called for both
    /// the doc sets where the user is the reviewer, as well as the reviewee.
    /// </summary>
    /// <param name="page"> The table to populate. </param>
    /// <param name="docSet"> The document set to process. </param>
    /// <param name="indentOneColumn"> Whether to indent the output by one column. </param>
    /// <param name="includeUserName"> Whether to include user name. </param>
    private void BuildReviewRow(Table page, DocumentSet docSet, bool indentOneColumn, bool includeUserName)
    {
        TableRow row = new TableRow();
        page.Rows.Add(row);

        // Spacer
        TableCell cell;
        if (indentOneColumn)
        {
            cell = new TableCell();
            row.Cells.Add(cell);
            cell.AppendCSSClass("CssReviewSpacerColumn");
        }

        // Time stamp
        cell = new TableCell();
        row.Cells.Add(cell);
        cell.AppendCSSClass("CssReviewTimestamp");
        cell.Text = WrapTimeStamp(docSet.TimeStamp);

        if (includeUserName)
        {
            cell = new TableCell();
            row.Cells.Add(cell);
            cell.AppendCSSClass("CssReviewAuthorColumn");
            cell.Text = docSet.UserAlias;
        }

        // Title
        cell = new TableCell();
        row.Cells.Add(cell);
        cell.AppendCSSClass("CssReviewTitle");

        HyperLink link = new HyperLink();
        link.NavigateUrl = Request.FilePath + "?dsid=" + docSet.Id;
        link.Text = docSet.Title;
        cell.Controls.Add(link);
    }

    /// <summary>
    /// Adds reviewers to the document set.
    /// </summary>
    /// <param name="dsid"> Document set id. </param>
    private void AddReviewers(int dsid)
    {
        DocumentSet[] dss = (from ds in DataContext.DocumentSets where ds.Id == dsid select ds).ToArray();
        if (dss.Length != 1)
        {
            ErrorOut("Could not find this document set in the system!");
            return;
        }

        DocumentSet docSet = dss[0];

        if (!docSet.UserAlias.EqualsIgnoreCase(AuthenticatedUserAlias))
        {
            ErrorOut("Only document set owner can add reviewers!");
            return;
        }


        DisplayPageHeader("New reviewers for " + Server.HtmlEncode(docSet.Title));

        AddLink("Back to document set.", "?dsid=" + dsid);
        Reviewer[] currentReviewers = (from rv in DataContext.Reviewers
                                       where rv.DocumentSetId == dsid
                                       select rv).ToArray();
        if (currentReviewers.Length > 0)
        {
            AddLabel("<br>Already reviewing this document set:<br>");
            foreach(Reviewer reviewer in currentReviewers)
                AddLabel("&nbsp;&nbsp;&nbsp;&nbsp;" + reviewer.UserAlias + "<br>");
        }

        AddLabel("Please add email aliases for reviewers you want to reach below:<br>");

        for (int i = 0; i < NumberOfReviewersToAdd; ++i)
        {
            TextBox b = new TextBox();
            ActivePage.Controls.Add(b);
            b.ID = "reviewer_" + i;
            b.AppendCSSClass("CssReviewerAlias");
            AddLabel("<br>");
        }

        Button submit = new Button();
        ActivePage.Controls.Add(submit);
        submit.AppendCSSClass("CssSubmitReivewers");
        submit.ID = "submit";
        submit.Text = "Submit";
        submit.Click += new EventHandler(delegate(object sender, EventArgs e)
            {
                SubmitReviewers(dsid);
            });
    }

    /// <summary>
    /// Submits new reviewers for the document set.
    /// </summary>
    /// <param name="dsid"> Document set id. </param>
    private void SubmitReviewers(int dsid)
    {
        bool error = false;
        List<string> emails = new List<string>();

        for (int i = 0; i < NumberOfReviewersToAdd; ++i)
        {
            TextBox b = (TextBox)FindControl("reviewer_" + i);
            if (b == null)
                continue;

            string email = b.Text;
            if (string.IsNullOrEmpty(email))
                continue;

            try
            {
                MailAddress m = new MailAddress(email + "@domain.com");
            }
            catch (FormatException)
            {
                error = true;
                ErrorOut("Invalid email: " + email);
                continue;
            }

            emails.Add(email);
        }

        if (error)
            return;

        foreach (string email in emails)
        {
            int? result = null;
            DataContext.AddReviewer(email, dsid, ref result);
        }

        Response.Redirect(Request.FilePath + "?dsid=" + dsid);
    }

    /// <summary>
    /// Sumbit an iteration of the document review, or a new comment.
    /// </summary>
    /// <param name="dsid"> Document set id. </param>
    /// <param name="isIteration"> True if this is an iteration (i.e. submitted by document set owner), false
    /// if this is a comment (submitted my someone else). </param>
    private void SubmitIterationOrComment(int dsid, bool isIteration)
    {
        DocumentSet[] dss = (from ds in DataContext.DocumentSets where ds.Id == dsid select ds).ToArray();
        if (dss.Length != 1)
        {
            ErrorOut("Could not find this document set in the system!");
            return;
        }

        DocumentSet docSet = dss[0];

        if (isIteration && !docSet.UserAlias.EqualsIgnoreCase(AuthenticatedUserAlias))
        {
            ErrorOut("Only document set owners can submit iterations!");
            return;
        }

        DisplayPageHeader(Server.HtmlEncode(docSet.Title));

        AddLink("Back to document set.", "?dsid=" + dsid);
        AddLabel("<br>");

        if (isIteration)
        {
            AddLabel("Document set title: ");
            TextBox title = new TextBox();
            ActivePage.Controls.Add(title);
            title.AppendCSSClass("CssTitleBox");
            title.ID = "title";
            title.Text = docSet.Title;
            title.TextMode = TextBoxMode.SingleLine;

            AddLabel("<br>Description:<br>");
            TextBox description = new TextBox();
            ActivePage.Controls.Add(description);
            description.AppendCSSClass("CssDescriptionBox");
            description.ID = "description";
            description.TextMode = TextBoxMode.MultiLine;
            description.Text = docSet.Description;
        }

        Table documents = new Table();
        ActivePage.Controls.Add(documents);
        documents.AppendCSSClass("CssFileLoader");

        var docQuery = from ff in DataContext.Documents where ff.DocumentSetId == dsid select ff;
        foreach (Document doc in docQuery)
        {
            if ((!isIteration) && (!doc.FileName.EndsWith(".docx", StringComparison.OrdinalIgnoreCase)))
                continue;

            TableRow r = new TableRow();
            documents.Rows.Add(r);
            TableCell c = new TableCell();
            r.Cells.Add(c);
            c.AppendCSSClass("CssDocumentName");
            c.Text = Server.HtmlEncode(doc.FileName);

            c = new TableCell();
            r.Cells.Add(c);
            c.AppendCSSClass("CssRemoveDocumentCheckboxCell");

            if (isIteration)
            {
                CheckBox cb = new CheckBox();
                c.Controls.Add(cb);
                cb.AppendCSSClass("CssRemoveDocumentCheckbox");
                cb.Text = "Remove";
                cb.ID = "remove_" + doc.Id;
                cb.Checked = !doc.IsActive;
            }

            r = new TableRow();
            documents.Rows.Add(r);
            c = new TableCell();
            r.Cells.Add(c);
            c.AppendCSSClass("CssDocumentUploaderCell");
            c.ColumnSpan = 2;
            FileUpload u = new FileUpload();
            c.Controls.Add(u);
            u.AppendCSSClass("CssDocumentUploader");
            u.ID = "upload_" + doc.Id;
        }

        if (isIteration)
        {
            TableRow r = new TableRow();
            documents.Rows.Add(r);
            TableCell c = new TableCell();
            r.Cells.Add(c);
            c.AppendCSSClass("CssNewLabelCell");
            c.ColumnSpan = 2;
            c.Text = "New files:";

            for (int i = 0; i < NumberOfFilesToAdd; ++i)
            {
                r = new TableRow();
                documents.Rows.Add(r);
                c = new TableCell();
                r.Cells.Add(c);
                c.AppendCSSClass("CssDocumentUploaderCell");
                c.ColumnSpan = 2;
                FileUpload u = new FileUpload();
                c.Controls.Add(u);
                u.AppendCSSClass("CssDocumentUploader");
                u.ID = "new_" + i;
            }
        }

        if (!isIteration)
        {
            if ((from rr in DataContext.Reviewers
                 where rr.UserAlias.Equals(AuthenticatedUserAlias) && rr.DocumentSetId == dsid
                 select rr).Count() > 0)
            {
                AddLabel("<br>Verdict:");
                DropDownList list = new DropDownList();
                ActivePage.Controls.Add(list);
                list.AppendCSSClass("CssVerdictList");
                list.ID = "verdict";

                ListItem item = new ListItem();
                item.Text = "Needs work";
                list.Items.Add(item);

                item = new ListItem();
                item.Text = "LGTM with minor tweaks";
                list.Items.Add(item);

                item = new ListItem();
                item.Text = "LGTM";
                list.Items.Add(item);

                item = new ListItem();
                item.Text = "Non-scoring comment";
                list.Items.Add(item);
            }
        }

        AddLabel("<br>Comment:<br>");
        TextBox comment = new TextBox();
        ActivePage.Controls.Add(comment);
        comment.AppendCSSClass("CssCommentBox");
        comment.ID = "comment";
        comment.TextMode = TextBoxMode.MultiLine;

        AddLabel("<br>");
        AddLink("Back to document set.", "?dsid=" + dsid);
        AddLabel("<br>");
        Button submit = new Button();
        ActivePage.Controls.Add(submit);
        submit.ID = "submit";
        submit.AppendCSSClass("CssSubmitIteration");
        submit.Text = "Submit";
        submit.Click += new EventHandler(delegate(object sender, EventArgs e)
            {
                submitIteration_Click(docSet, isIteration);
            });
    }


    /// <summary>
    /// Submits an iteration or a review.
    /// </summary>
    /// <param name="docSet"> Document Set. </param>
    /// <param name="isIteration"> Whether this is an iteration (submitted by the author),
    /// or a review (submitted by a reviewer). </param>
    void submitIteration_Click(DocumentSet docSet, bool isIteration)
    {
        StringBuilder defaultComment = new StringBuilder();

        if (isIteration)
        {
            TextBox titleBox = (TextBox)FindControl("title");
            string title = titleBox.Text;
            if (string.IsNullOrEmpty(title))
            {
                ErrorOut("Need a title!");
                return;
            }
            TextBox descriptionBox = (TextBox)FindControl("description");
            string description = descriptionBox.Text;

            if (title != docSet.Title || description != docSet.Description)
                DataContext.UpdateDocumentSet(docSet.Id, title, description);
        }

        var docQuery = from ff in DataContext.Documents where ff.DocumentSetId == docSet.Id select ff;
        foreach (Document doc in docQuery)
        {
            FileUpload fu = (FileUpload)FindControl("upload_" + doc.Id);
            if (isIteration)
            {
                CheckBox remove = (CheckBox)FindControl("remove_" + doc.Id);
                if (remove.Checked != !doc.IsActive)
                    DataContext.SetDocumentActiveBit(doc.Id, !remove.Checked);

                if (fu.HasFile)
                {
                    string tempFile = Path.GetTempFileName();
                    try
                    {
                        fu.SaveAs(tempFile);
                        if (DocumentLocker.IsLocked(tempFile))
                        {
                            MergeComments(doc, null, fu.FileName, tempFile);
                        }
                        else
                        {
                            if (doc.FileName.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
                                DocumentManager.DocumentLocker.Lock(tempFile);
                            byte[] newFile = File.ReadAllBytes(tempFile);
                            int? verid = null;
                            DataContext.AddVersion(newFile, doc.Id, ref verid);
                            defaultComment.Append("Updated " + doc.FileName + "\n");
                        }
                    }
                    finally
                    {
                        File.Delete(tempFile);
                    }
                }
            }
            else
            {
                if (fu != null && fu.HasFile && doc.FileName.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
                {
                    if (MergeComments(doc, fu, fu.FileName, null))
                        defaultComment.Append("Commented on  " + doc.FileName + "\n");
                }
            }
        }

        if (isIteration)
        {
            for (int i = 0; i < NumberOfFilesToAdd; ++i)
            {
                FileUpload fu = (FileUpload)FindControl("new_" + i);
                if (fu.HasFile)
                {
                    string fileName = Path.GetFileName(fu.FileName);
                    int? docid = null;
                    DataContext.AddDocument(fileName, docSet.Id, ref docid);
                    string tempFile = Path.GetTempFileName();
                    try
                    {
                        fu.SaveAs(tempFile);
                        if (fileName.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
                            DocumentManager.DocumentLocker.Lock(tempFile);
                        byte[] newFile = File.ReadAllBytes(tempFile);
                        int? verid = null;
                        DataContext.AddVersion(newFile, docid, ref verid);
                    }
                    finally
                    {
                        File.Delete(tempFile);
                    }

                    defaultComment.Append("Added new file: " + fileName + "\n");
                }
            }
        }

        byte verdict = 3; // Non-scoring comment
        if (!isIteration)
        {
            DropDownList verdictList = (DropDownList)FindControl("verdict");
            verdict = (byte)verdictList.SelectedIndex;
        }

        TextBox commentBox = (TextBox)FindControl("comment");
        string comment = string.IsNullOrEmpty(commentBox.Text) ? defaultComment.ToString() : commentBox.Text;

        DataContext.AddReview(docSet.Id, verdict, comment);
        Response.Redirect(Request.FilePath + "?dsid=" + docSet.Id);
    }

    /// <summary>
    /// Merges comments from a newly uploaded file into the stored version.
    /// </summary>
    /// <param name="doc"> Document. </param>
    /// <param name="fu"> File uploader, or null if the file is already saved. </param>
    /// <param name="uploadedFileName"> Uploaded file name. </param>
    /// <param name="newFile"> File name if the file has been already saved, or null. </param>
    /// <returns> True if the merge succeeded. </returns>
    private bool MergeComments(Document doc, FileUpload fu, string uploadedFileName, string newFile)
    {
        bool result = false;
        string dotVersion = Path.GetExtension(Path.GetFileNameWithoutExtension(uploadedFileName));
        int vid = 0;
        if (dotVersion != null && int.TryParse(dotVersion.Substring(1), out vid))
        {
            if (fu != null)
                newFile = Path.GetTempFileName();

            string oldFile = Path.GetTempFileName();
            try
            {
                if (fu != null)
                    fu.SaveAs(newFile);

                System.Data.Linq.Binary documentVersion = (from vv in DataContext.DocumentVersions
                                                           where vv.Id == vid
                                                           select vv.Contents).Single();

                File.WriteAllBytes(oldFile, documentVersion.ToArray());
                DocumentUtils.MergeComments(oldFile, newFile);
                DocumentUtils.RemoveDuplicateComments(oldFile);
                byte[] bytes = File.ReadAllBytes(oldFile);

                DataContext.UpdateVersion(bytes, vid);
                result = true;
            }
            finally
            {
                if (fu != null)
                    File.Delete(newFile);
                File.Delete(oldFile);
            }
        }

        return result;
    }

    /// <summary>
    /// Displays the document set.
    /// </summary>
    /// <param name="dsid"> Document set id. </param>
    private void DisplayDocumentSet(int dsid)
    {
        DocumentSet[] dss = (from ds in DataContext.DocumentSets where ds.Id == dsid select ds).ToArray();
        if (dss.Length != 1)
        {
            ErrorOut("Could not find this document set in the system!");
            return;
        }

        DocumentSet docSet = dss[0];
        DisplayPageHeader(Server.HtmlEncode(docSet.Title));

        bool iOwnTheChange = docSet.UserAlias.EqualsIgnoreCase(AuthenticatedUserAlias);

        Table page = new Table();
        ActivePage.Controls.Add(page);
        page.EnableViewState = false;

        AddDocSetDescriptionRow(page, "Date:", WrapTimeStamp(docSet.TimeStamp));
        AddDocSetDescriptionRow(page, "User:", docSet.UserAlias);
        AddDocSetDescriptionRow(page, "Status:", docSet.Status == 0 ? "In review" :
            (docSet.Status == 2 ? "Closed" : "Deleted"));
        AddDocSetDescriptionRow(page, "Description:", "<pre>" + Server.HtmlEncode(docSet.Description) + "</pre>");
        AddDocSetDescriptionRow(page, "Files:", "");

        var docQuery = from fl in DataContext.Documents where fl.DocumentSetId == dsid select fl;

        foreach (Document doc in docQuery)
        {
            AddDocumentRow(page, doc, iOwnTheChange);
        }

        AddLabel("<p>Do NOT change the file names when you download the documents!<br>They must be uploaded " +
            "using the exact same file names.</p>", "CssImportantNote");

        AddLabel("<h3>Review history</h3>");
        Table reviewResults = new Table();
        ActivePage.Controls.Add(reviewResults);
        reviewResults.EnableViewState = false;

        reviewResults.AppendCSSClass("CssReviewHistory");

        var allReviewsQuery = from rr in DataContext.Reviews
                              where rr.DocumentSetId == dsid
                              orderby rr.TimeStamp
                              select rr;
        foreach (Review review in allReviewsQuery)
        {
            TableRow row = new TableRow();
            reviewResults.Rows.Add(row);

            row.AppendCSSClass("CssTopAligned");

            TableCell dateCell = new TableCell();
            row.Cells.Add(dateCell);

            dateCell.AppendCSSClass("CssReviewHistoryDate");
            dateCell.Text = WrapTimeStamp(review.TimeStamp);

            TableCell nameCell = new TableCell();
            row.Cells.Add(nameCell);

            nameCell.AppendCSSClass("CssReviewHistoryName");
            nameCell.Text = review.UserAlias;

            TableCell verdictCell = new TableCell();
            row.Cells.Add(verdictCell);

            verdictCell.AppendCSSClass("CssReviewHistoryScore");
            verdictCell.Text = ReviewStatusToString(review.Verdict);

            if (review.Comment != null)
            {
                TableCell commentCell = new TableCell();
                row.Cells.Add(commentCell);
                commentCell.AppendCSSClass("CssReviewHistoryComment");
                commentCell.Text = review.Comment;
            }
        }

        if (docSet.Status != 0)
        {
            AddLabel(String.Format("<br>This review has been {0}.", (docSet.Status == 2 ? "closed" : "deleted")));
            return;
        }

        AddLabel("<h3>Vote so far</h3>");

        Table reviewerVote = new Table();
        ActivePage.Controls.Add(reviewerVote);
        reviewerVote.EnableViewState = false;

        var reviewerQuery = from rv in DataContext.Reviewers where rv.DocumentSetId == dsid select rv;
        Reviewer[] reviewers = reviewerQuery.ToArray();
        bool iAmAReviewer = false;
        foreach (Reviewer reviewer in reviewers)
        {
            if (AuthenticatedUserAlias.EqualsIgnoreCase(reviewer.UserAlias))
            {
                iAmAReviewer = true;
                continue;
            }

            TableRow row = new TableRow();
            reviewerVote.Rows.Add(row);

            TableCell nameCell = new TableCell();
            row.Cells.Add(nameCell);
            nameCell.Text = reviewer.UserAlias;

            TableCell verdictCell = new TableCell();
            row.Cells.Add(verdictCell);

            byte[] myVerdicts = (from rv in DataContext.Reviews
                                 where rv.DocumentSetId == dsid && rv.UserAlias == reviewer.UserAlias
                                 orderby rv.Id
                                 select rv.Verdict).ToArray();
            verdictCell.Text = myVerdicts.Length == 0 ? "Have not looked" : ReviewStatusToString(myVerdicts.Last());
        }

        AddLabel("<br>");

        if (!iAmAReviewer && !iOwnTheChange)
        {
            AddLink("I would like to review this change.", "?dsid=" + docSet.Id + "&action=makemereviewer");
        }
        else if (iOwnTheChange)
        {
            AddLink("Update", "?dsid=" + docSet.Id + "&action=iterate", "CssDocumentSetActionLink");
            AddLink("Close", "?dsid=" + docSet.Id + "&action=close", "CssDocumentSetActionLink");
            AddLink("Delete", "?dsid=" + docSet.Id + "&action=delete", "CssDocumentSetActionLink");
            AddLink("Add reviewers", "?dsid=" + docSet.Id + "&action=addreviewers", "CssDocumentSetActionLink");
        }
        else
        {
            AddLink("Comment", "?dsid=" + docSet.Id + "&action=comment", "CssDocumentSetActionLink");
        }
    }

    /// <summary>
    /// Maps verdict code to string.
    /// </summary>
    /// <param name="status"> Review verdict. </param>
    /// <returns> String representation of status. </returns>
    private static string ReviewStatusToString(int status)
    {
        switch (status)
        {
            case 0: return "Needs work";
            case 1: return "LGTM with minor tweaks";
            case 2: return "LGTM";
        }
        return "Non-scoring comment";
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="page"> The table that controls the format of our page. </param>
    /// <param name="doc"> The document. </param>
    /// <param name="iOwnTheChange"> True if the current user is the owner of the change. </param>
    private void AddDocumentRow(Table page, Document doc, bool iOwnTheChange)
    {
        TableRow row = new TableRow();
        page.Rows.Add(row);
        row.AppendCSSClass("CssTopAligned");

        TableCell cell = new TableCell();
        row.Cells.Add(cell);

        cell = new TableCell();
        row.Cells.Add(cell);

        cell.Text = Server.HtmlEncode(doc.FileName);
        if (!doc.IsActive)
            cell.AppendCSSClass("CssDeletedFile");

        var versionsQuery = from vv in DataContext.DocumentVersions
                            where vv.DocumentId == doc.Id
                            select new { vv.Id, vv.TimeStamp };
        foreach(var version in versionsQuery)
        {
            row = new TableRow();
            page.Rows.Add(row);
            row.AppendCSSClass("CssTopAligned");

            cell = new TableCell();
            row.Cells.Add(cell);

            cell = new TableCell();
            row.Cells.Add(cell);

            HyperLink l = new HyperLink();
            l.AppendCSSClass("CssDocumentTimeStamp");
            cell.Controls.Add(l);
            l.Text = WrapTimeStamp(version.TimeStamp);
            l.NavigateUrl = Request.FilePath + "?vid=" + version.Id;

            if (iOwnTheChange)
            {
                l = new HyperLink();
                l.AppendCSSClass("CssDocumentWriteableCopy");
                cell.Controls.Add(l);
                l.Text = "writeable copy";
                l.NavigateUrl = Request.FilePath + "?vid=" + version.Id + "&action=unlock";
            }
        }
    }

    /// <summary>
    /// Adds two rows with two cells to a table with the specified text in the cells.
    /// </summary>
    /// <param name="page"> The table to which the row is added. </param>
    /// <param name="cell1"> The text for the first cell (usually, a description). </param>
    /// <param name="cell2"> The text for the second cell (usually, the information). </param>
    private void AddDocSetDescriptionRow(Table page, string cell1, string cell2)
    {
        TableRow row = new TableRow();
        page.Rows.Add(row);
        row.AppendCSSClass("CssTopAligned");

        TableCell cell = new TableCell();
        row.Cells.Add(cell);

        cell.Text = cell1;

        cell = new TableCell();
        row.Cells.Add(cell);

        cell.Text = cell2;
    }

    /// <summary>
    /// Add a user to the list of reviewers.
    /// </summary>
    /// <param name="dsid"> Document Set Id. </param>
    private void AddToReviewers(int dsid, string userName)
    {
        DocumentSet[] dss = (from ds in DataContext.DocumentSets where ds.Id == dsid select ds).ToArray();
        if (dss.Length != 1)
        {
            ErrorOut("Could not find this document set in the system!");
            return;
        }

        if (dss[0].Status != 0)
        {
            AddLabel("This document set is no longer active.");
            RedirectToDocumentSet(dsid, dss[0].Title);
            return;
        }

        int? result = null;
        DataContext.AddReviewer(userName, dsid, ref result);

        AddLabel("Thank you for agreeing to review this change list!");
        RedirectToDocumentSet(dsid, dss[0].Title);
    }

    /// <summary>
    /// Generates HTML to redirect the user to the document set page.
    /// </summary>
    /// <param name="dsid"> The document set to show. </param>
    /// <param name="title"> The name of the document set. </param>
    private void RedirectToDocumentSet(int dsid, string title)
    {
        AddLabel("<br>");
        AddLink("Click here to continue to " + Server.HtmlEncode(title), "?dsid=" + dsid);
    }

    /// <summary>
    /// Displays context-sensitive help.
    /// </summary>
    /// <param name="sourceUrl"> The URL from which the request came. </param>
    private void DisplayHelp(string sourceUrl)
    {
        AddLabel("<h1>Black Square help</h1>");

        string url = Server.UrlDecode(sourceUrl);
        HyperLink l = new HyperLink();
        ActivePage.Controls.Add(l);
        l.Text = "Back...";
        l.NavigateUrl = url;

        AddLabel("<br><br>");

        if (url.Contains("ds")) // Document set
        {
            AddLabel("<p>This page displays the details of a document set, " +
                "the history of the review iterations, and the current vote.</p>");

            AddLabel("<p>Most importantly, it allows both the reviewers as well as casual browsers to submit " +
                "the reviews and reviewees to submit iterations.</p>");

            AddLabel("<p>A <b>review</b> is a collection of comments made in the documents " +
                "accompanied by one top level comment, which can be entered on this page.</p>");

            AddLabel("<p>An <b>iteration</b> is an update to the documents that is made by the reviewee in response " +
                "to the reviewers' comments.</p>");

            AddLabel("<p>Usually, most of the comments are made in the documents themselves. When a review iteration " +
                "is submitted, the documents are uploaded, and the comments in them are merged into master versions " +
                "stored on the server. After this merge, the comments from all reviewers become visible to everybody." +
                "</p>");

            AddLabel("<p>If a review iteration comes from a reviewer, the reviewer has an option to enter a vote. " +
                "A vote can be one of:</p>");

            Table t = new Table();
            ActivePage.Controls.Add(t);
            t.Width = new Unit(100, UnitType.Percentage);

            TableRow r = new TableRow();
            t.Rows.Add(r);
            TableCell c = new TableCell();
            r.Cells.Add(c);
            c.Text = "LGTM";
            c = new TableCell();
            r.Cells.Add(c);
            c.Text = "'Looks Good To Me' - in the reviewer's opinion, the change is ready to be submitted. " +
                "No further iterations are necessary.";

            r = new TableRow();
            t.Rows.Add(r);
            c = new TableCell();
            r.Cells.Add(c);
            c.Text = "LGTM with minor tweaks";
            c = new TableCell();
            r.Cells.Add(c);
            c.Text = "The reviewer has recommended a few changes, but does not feel strongly about them - these are " +
                "mere suggestions. The change can be checked in with or without following the recommendations, " +
                "and no further iterations are necessary.";

            r = new TableRow();
            t.Rows.Add(r);
            c = new TableCell();
            r.Cells.Add(c);
            c.Text = "Needs work";
            c = new TableCell();
            r.Cells.Add(c);
            c.Text = "The reviewer things that the change is not ready yet. The comments in this review iteration " +
                "should be addressed, and the reviewer should be allowed to re-examine the result. The review should " +
                "not be closed until there is even one vote that says 'Needs work'.";

            r = new TableRow();
            t.Rows.Add(r);
            c = new TableCell();
            r.Cells.Add(c);
            c.Text = "Non-scoring comment";
            c = new TableCell();
            r.Cells.Add(c);
            c.Text = "The reviewer withholds his or her opinion. This type of vote is typically used to abrogate the " +
                "responsibility for the code review - for example, if the reviewer is very busy.</p>";

            AddLabel("<p>In addition to reviewers and reviewees, any casual browser can enter comments. " +
                "However, they can not vote on the review. Their review iterations are filed with the 'Non-scoring' " +
                "vote. However, anybody (except for the people who are already part of the review) can join " +
                "the review and become the 'official' reviewer by following 'I want to review this change' link.</p>");

            AddLabel("<p>If you are a reviewer, click on the last date hyperlink under the document " +
                "name to download the latest version of the document. Save it locally on your computer, but do " +
                "NOT change the default name - it has a specific format that helps the system identify the document. " +
                "Use Microsoft Word Review tab to add comments to the document. Once you are done, come back to this " +
                "page and click on 'Comment' hyperlink. The following page will allow you to submit your review.</p>");

            AddLabel("<p>If you are a reviewee, you can download a 'writeable copy' of the document that contains " + 
                "all the comments from all the reviewers, but will allow you to edit the text of the document. " +
                "When you are done making changes, upload the new documents using the 'Upload' hyperlink. Uploading " +
                "writeable documents will create new versions as opposed to just merging the comments in. " +
                "Of course if you just want to answer the comments without creating a new iteration, you can follow " +
                "exactly the same path as the reviewers to submit your response. Use the instructions in the previous" +
                "paragraph, except the hyperlik will still read 'Update'.</p>");
        }
        else // Dashboard
        {
            AddLabel("<h2>Welcome to Black Square!</h2>");

            AddLabel("<p>Black Square is a web-based document review system designed for use by individuals " +
                "and small teams. Its goal is making the cost of a comment as close to zero as possible: easy " +
                "commenting encourages thorough reviews.</p>");

            AddLabel("Reviewing code in Black Square is easy indeed. A reviewer downloads a read-only version of " +
                "a word document (currently, only .docx files are supported for commenting), makes comments " +
                "using Office commenting features, and uploads the document back to the Black Square. " +
                "The server then merges the comments from the uploaded document to the master copy kept on server, " +
                "which makes comments entered by everyone visible to all other reviewers and the reviewee.</p>");

            AddLabel("<br><br><br>");

            AddLabel("<p><b>Note:</b> Black Square help is context-sensitive: clicking on the help link on every " +
                "page where it is available will give you information on how to use that page.</p>");

            AddLabel("<br>");

            AddLabel("<p>This page shows the list of currently active reviews for the logged (or explicitly " +
                "requested) user, as well as the recent history of the code reviews that have been completed.</p>");

            AddLabel("<p>To look up a dashboard for someone else, specify that user's name " +
                "using the query parameter, like this: " + Request.FilePath + "?alias=useralias</p>");

            AddLabel("<p>To look up all active reviews, do this:" + Request.FilePath + "?alias=*</p>");

            AddLabel("<p>Otherwise, click on any of the links to see the details of the document sets and the " +
                "review comments for it.</p>");
        }

        AddLabel("<br><br>");

        l = new HyperLink();
        ActivePage.Controls.Add(l);
        l.Text = "Back...";
        l.NavigateUrl = url;
    }

    /// <summary>
    /// Inserts the header HTML file into the page. This allows skinning (branding) of Black Square to integrate it
    /// seamlessly into existing tool chain.
    /// 
    /// The contents of the file are dumped in the very beginning of the page as is.
    /// </summary>
    public void RenderHeaderSkin()
    {
        string skinDir = System.Configuration.ConfigurationSettings.AppSettings["skinDirectory"];
        if (skinDir == null)
            return;

        string fileName = System.IO.Path.Combine(skinDir, "header.html");
        if (System.IO.File.Exists(fileName))
            Response.WriteFile(fileName);
    }

    /// <summary>
    /// Inserts the footer HTML file into the page. This allows skinning (branding) of Black Square to integrate it
    /// seamlessly into existing tool chain.
    /// 
    /// The contents of the file are dumped in the very end of the page as is.
    /// </summary>
    public void RenderFooterSkin()
    {
        string skinDir = System.Configuration.ConfigurationSettings.AppSettings["skinDirectory"];
        if (skinDir == null)
            return;

        string fileName = System.IO.Path.Combine(skinDir, "footer.html");
        if (System.IO.File.Exists(fileName))
            Response.WriteFile(fileName);
    }
}
